//
//  RCloudFMDatabasePool.m
//  fmdb
//
//  Created by August Mueller on 6/22/11.
//  Copyright 2011 Flying Meat Inc. All rights reserved.
//

#if RCLOUD_FMDB_SQLITE_STANDALONE
#import <sqlite3/sqlite3.h>
#else
#import <sqlite3.h>
#endif

#import "RCloudFMDatabase.h"

@interface RCloudFMDatabasePool ()

- (void)pushDatabaseBackInPool:(RCloudFMDatabase *)db;
- (RCloudFMDatabase *)db;

@end

@implementation RCloudFMDatabasePool
@synthesize path = _path;
@synthesize delegate = _delegate;
@synthesize maximumNumberOfDatabasesToCreate = _maximumNumberOfDatabasesToCreate;
@synthesize openFlags = _openFlags;

+ (instancetype)databasePoolWithPath:(NSString *)aPath {
    return RCloudFMDBReturnAutoreleased([[self alloc] initWithPath:aPath]);
}

+ (instancetype)databasePoolWithPath:(NSString *)aPath flags:(int)openFlags {
    return RCloudFMDBReturnAutoreleased([[self alloc] initWithPath:aPath flags:openFlags]);
}

- (instancetype)initWithPath:(NSString *)aPath flags:(int)openFlags {

    self = [super init];

    if (self != nil) {
        _path = [aPath copy];
        _lockQueue = dispatch_queue_create([[NSString stringWithFormat:@"fmdb.%@", self] UTF8String], NULL);
        _databaseInPool = RCloudFMDBReturnRetained([NSMutableArray array]);
        _databaseOutPool = RCloudFMDBReturnRetained([NSMutableArray array]);
        _openFlags = openFlags;
    }

    return self;
}

- (instancetype)initWithPath:(NSString *)aPath {
    // default flags for sqlite3_open
    return [self initWithPath:aPath flags:SQLITE_OPEN_FULLMUTEX | SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE];
}

- (instancetype)init {
    return [self initWithPath:nil];
}

- (void)dealloc {

    _delegate = 0x00;
    RCloudFMDBRelease(_path);
    RCloudFMDBRelease(_databaseInPool);
    RCloudFMDBRelease(_databaseOutPool);

    if (_lockQueue) {
        RCloudFMDBDispatchQueueRelease(_lockQueue);
        _lockQueue = 0x00;
    }
#if !__has_feature(objc_arc)
    [super dealloc];
#endif
}

- (void)executeLocked:(void (^)(void))aBlock {
    dispatch_sync(_lockQueue, aBlock);
}

- (void)pushDatabaseBackInPool:(RCloudFMDatabase *)db {

    if (!db) { // db can be null if we set an upper bound on the # of databases to create.
        return;
    }

    [self executeLocked:^() {

        if ([self->_databaseInPool containsObject:db]) {
            [[NSException
                exceptionWithName:@"Database already in pool"
                           reason:@"The RCloudFMDatabase being put back into the pool is already present in the pool"
                         userInfo:nil] raise];
        }

        [self->_databaseInPool addObject:db];
        [self->_databaseOutPool removeObject:db];

    }];
}

- (RCloudFMDatabase *)db {

    __block RCloudFMDatabase *db;

    [self executeLocked:^() {
        db = [self->_databaseInPool lastObject];

        BOOL shouldNotifyDelegate = NO;

        if (db) {
            [self->_databaseOutPool addObject:db];
            [self->_databaseInPool removeLastObject];
        } else {

            if (self->_maximumNumberOfDatabasesToCreate) {
                NSUInteger currentCount = [self->_databaseOutPool count] + [self->_databaseInPool count];

                if (currentCount >= self->_maximumNumberOfDatabasesToCreate) {
                    NSLog(@"Maximum number of databases (%ld) has already been reached!", (long)currentCount);
                    return;
                }
            }

            db = [RCloudFMDatabase databaseWithPath:self->_path];
            shouldNotifyDelegate = YES;
        }

// This ensures that the db is opened before returning
#if SQLITE_VERSION_NUMBER >= 3005000
        BOOL success = [db openWithFlags:self->_openFlags];
#else
        BOOL success = [db open];
#endif
        if (success) {
            if ([self->_delegate respondsToSelector:@selector(databasePool:shouldAddDatabaseToPool:)] &&
                ![self->_delegate databasePool:self shouldAddDatabaseToPool:db]) {
                [db close];
                db = 0x00;
            } else {
                // It should not get added in the pool twice if lastObject was found
                if (![self->_databaseOutPool containsObject:db]) {
                    [self->_databaseOutPool addObject:db];

                    if (shouldNotifyDelegate &&
                        [self->_delegate respondsToSelector:@selector(databasePool:didAddDatabase:)]) {
                        [self->_delegate databasePool:self didAddDatabase:db];
                    }
                }
            }
        } else {
            NSLog(@"Could not open up the database at path %@", self->_path);
            db = 0x00;
        }
    }];

    return db;
}

- (NSUInteger)countOfCheckedInDatabases {

    __block NSUInteger count;

    [self executeLocked:^() {
        count = [self->_databaseInPool count];
    }];

    return count;
}

- (NSUInteger)countOfCheckedOutDatabases {

    __block NSUInteger count;

    [self executeLocked:^() {
        count = [self->_databaseOutPool count];
    }];

    return count;
}

- (NSUInteger)countOfOpenDatabases {
    __block NSUInteger count;

    [self executeLocked:^() {
        count = [self->_databaseOutPool count] + [self->_databaseInPool count];
    }];

    return count;
}

- (void)releaseAllDatabases {
    [self executeLocked:^() {
        [self->_databaseOutPool removeAllObjects];
        [self->_databaseInPool removeAllObjects];
    }];
}

- (void)inDatabase:(void (^)(RCloudFMDatabase *db))block {

    RCloudFMDatabase *db = [self db];

    block(db);

    [self pushDatabaseBackInPool:db];
}

- (void)beginTransaction:(BOOL)useDeferred withBlock:(void (^)(RCloudFMDatabase *db, BOOL *rollback))block {

    BOOL shouldRollback = NO;

    RCloudFMDatabase *db = [self db];

    if (useDeferred) {
        [db beginDeferredTransaction];
    } else {
        [db beginTransaction];
    }

    block(db, &shouldRollback);

    if (shouldRollback) {
        [db rollback];
    } else {
        [db commit];
    }

    [self pushDatabaseBackInPool:db];
}

- (void)inDeferredTransaction:(void (^)(RCloudFMDatabase *db, BOOL *rollback))block {
    [self beginTransaction:YES withBlock:block];
}

- (void)inTransaction:(void (^)(RCloudFMDatabase *db, BOOL *rollback))block {
    [self beginTransaction:NO withBlock:block];
}

- (NSError *)inSavePoint:(void (^)(RCloudFMDatabase *db, BOOL *rollback))block {
#if SQLITE_VERSION_NUMBER >= 3007000
    static unsigned long savePointIdx = 0;

    NSString *name = [NSString stringWithFormat:@"savePoint%ld", savePointIdx++];

    BOOL shouldRollback = NO;

    RCloudFMDatabase *db = [self db];

    NSError *err = 0x00;

    if (![db startSavePointWithName:name error:&err]) {
        [self pushDatabaseBackInPool:db];
        return err;
    }

    block(db, &shouldRollback);

    if (shouldRollback) {
        // We need to rollback and release this savepoint to remove it
        [db rollbackToSavePointWithName:name error:&err];
    }
    [db releaseSavePointWithName:name error:&err];

    [self pushDatabaseBackInPool:db];

    return err;
#else
    NSString *errorMessage = NSLocalizedString(@"Save point functions require SQLite 3.7", nil);
    if (self.logsErrors)
        NSLog(@"%@", errorMessage);
    return [NSError errorWithDomain:@"RCloudFMDatabase" code:0 userInfo:@{NSLocalizedDescriptionKey : errorMessage}];
#endif
}

@end
